#pragma once
#include <string>
#include <fstream>
#include <vector>
#include <iostream>
#include <unordered_map>
#include <mutex>
#include "util.hpp"
#include "log.hpp"
namespace ns_index
{
	struct DocInfo
	{
		std::string title;  //文档的标题
		std::string content;//文档去标签后的内容
		std::string url;	//文档的url
		uint64_t doc_id;    //将来文档的ID
	};

	struct InvertedElem
	{
		uint64_t doc_id;    //文档的ID
		std::string word;	//关键字
		int weight;	 		//权重
	};
	//倒排拉链：一个vector内存放着与一个关键字对应的文档
	typedef std::vector<InvertedElem> InvertedList;

	class Index
	{
	public:
		~Index() {}
		//根据doc_id找到对应的文档内容
		DocInfo* GetForwardindex(uint64_t doc_id)
		{
			if(doc_id >= forward_index.size())
			{
				//std::cerr << "doc_id out range,error!" << std::endl;
				LOG(FATAL,"doc_id out range,error!");
				return nullptr;
			}
			return &forward_index[doc_id];
		}

		//根据关键字word，获得倒排拉链
		InvertedList* GetInvertedList(const std::string& word)
		{
			auto iter = inverted_index.find(word);
			if(iter == inverted_index.end())
			{
				//std::cerr << word << " have no invertedList!" << std::endl;
				LOG(FATAL,word + " have no invertedList!");
				return nullptr;
			}
			return &(iter->second);
		}

		//根据去标签格式化后的文档，构建正排和倒排索引（把文件内容传入打开，在内部构建正排和倒排索引）
		//data/raw_html/raw.txt
		bool BuildIndex(const std::string& input)
		{
			std::ifstream in(input,std::ios::in | std::ios::binary);
			if(!in.is_open())
			{
				std::cerr << "sorry, " << input << " open failed" << std::endl;
				return false;
			}
			std::string line;
			int count = 0;
			while(std::getline(in,line))
			{
				DocInfo* doc = BuildForwardIndex(line);
				if(nullptr == doc)
				{
					std::cerr << "build " << line << " error!" << std::endl;
					continue;
				}

				BuildInvertedIndex(*doc);
				count++;
				//if(count % 50 == 0)
					//std::cout << "已建立的索引数: " << count << std::endl;
				LOG(NORMAL,"已建立的索引文档数："+std::to_string(count));			
			}
			/*for(auto e : inverted_index[" "])
				std::cout << e.doc_id << std::endl;*/
			//后来实践证明：空格会被jieba分词保留，那么就会导致倒排拉链中空格对应所有文件(每个文件必有空格)。如果在搜索时含有空格，这个搜索的空格也会被保留，从而结果出现所有文件！！因此，倒排拉链中的空格要手动删除。
			//inverted_index.erase(inverted_index.find(std::string(" ")));
			return true;
		}
		
		//获取单例指针
		static Index* GetInstance()
		{
			if(nullptr == instance)
			{
				mtx.lock();//加锁维护线程安全
				if(nullptr == instance)
				{
					instance = new Index();
				}
				mtx.unlock();
			}
			return instance;
		}
	private:
		//单例模式保证
		Index() {}	//构造函数即使为空也要有函数体，不能为delete，方便构造对象！
		Index(const Index&) = delete;
		Index& operator=(const Index&) = delete;

		static Index* instance;
		static std::mutex mtx;	//线程锁维护线程安全

		//正排索引和倒排索引
		//正排索引：由文档ID找到文档内容
		//倒排索引：根据文档内容分词，整理不重复的各个关键词，对应联系到文档ID
		
		//正排索引的数据结构用数组，因为数组的下标天然是文档的ID！
		std::vector<DocInfo> forward_index;

		//倒排索引一定是一个关键字和一组InvertedElem对应，故我们用unordered_map(关键字和倒排拉链的映射关系）
		std::unordered_map<std::string,InvertedList> inverted_index;

		DocInfo* BuildForwardIndex(const std::string& line)
		{
			//解析line，进行字符串切分
			std::vector<std::string> results;
			const std::string sep = "\3";
			ns_util::StringUtil::Split(line,&results,sep);
			if(results.size() != 3)
				return nullptr;

			//字符串填充进DocInfo
			DocInfo doc;
			doc.title = results[0];
			doc.content = results[1];
			doc.url = results[2];
			doc.doc_id = forward_index.size();

			//插入到正排索引的vector
			forward_index.push_back(std::move(doc));
			return &forward_index.back();
		}
		bool BuildInvertedIndex(const DocInfo& doc)
		{
			//根据文档内容(title,content,url)，形成一个或多个InvertedElem（倒排拉链）
			//因为当前是一个一个文档进行处理的，一个文档会包含多个“词”，都应当对应到当前的doc_id，所以要先对title和content进行分词! 
			//利用jieba进行分词，然后进行词频统计
			struct word_cnt
			{
				//词频统计结构体
				int title_cnt;	//标题词频
				int content_cnt;//内容词频

				word_cnt()
				:title_cnt(0)
				,content_cnt(0)
				{}
			};
			std::unordered_map<std::string,word_cnt> word_map;//暂存词频的映射表

			//统计标题词频
			std::vector<std::string> title_words;
			ns_util::JiebaUtil::CutString(doc.title,&title_words);
			
			for(std::string s:title_words)
			{
				//注意：在实际的搜索中，用户的大小写数据都是一视同仁的，所以这里应该对用户输入的数据做全部小写的处理，这也是为什么循环不用引用的原因，因为不想改变原数组内容。
				boost::to_lower(s);
				word_map[s].title_cnt++;
			}
			//统计正文词频
			std::vector<std::string> content_words;
			ns_util::JiebaUtil::CutString(doc.content,&content_words);
			
			for(std::string s:content_words)
			{	
				//这里同上
				boost::to_lower(s);
				word_map[s].content_cnt++;
			}

			//同时也要注意词和文档的相关性。这里简单采用词频。具体逻辑：在标题中出现的词，可以认为相关性更高一些，而在内容中出现，相关性要低一些！
			
			//遍历word_map构建倒排索引
#define X 10
#define Y 1
			for(auto& word_pair : word_map)
			{	
				InvertedElem item;
				item.doc_id = doc.doc_id;
				item.word = word_pair.first;
				//权重，标题出现权重更大
				item.weight = X*word_pair.second.title_cnt + Y*word_pair.second.content_cnt;

				InvertedList& inverted_list = inverted_index[word_pair.first];
				inverted_list.push_back(std::move(item));
			}
			
			return true;
		}
	};
	Index* Index::instance = nullptr;
	std::mutex Index::mtx;
}













